import { Observable, defer, catchError, tap, map, of, concatMap, forkJoin, } from "rxjs"
import axios, { RawAxiosRequestHeaders } from "axios"
import * as yaml from "js-yaml"
import * as path from "path"
import * as fs from "fs-extra"
import * as moment from "moment"
import IHelper from "@/services/helper.service"
import { IWonewSer } from "@/typing"
import ILogger from "@/services/logger.service"
import { HttpException, HttpStatus } from "@nestjs/common"

class _ClashHelper {
  groups: IWonewSer.ProxyGroup[] = []
  proxies: IWonewSer.ClashProxy[] = []
  proxyConfigPath: string = ""

  constructor() {
    this.initGroups()
  }

  initGroups() {
    this.groups = [
      {id: "bili", port: 6001, apiPort: 6002, secret: "bilibili", checkUrl: "https://bilibili.com", fireCmd: "", state: 0},
      {id: "dy", port: 6003, apiPort: 6004, secret: "douyin", checkUrl: "https://douyin.com", fireCmd: "", state: 0},
      {id: "ks", port: 6005, apiPort: 6006, secret: "kuaishou", checkUrl: "https://kuaishou.com", fireCmd: "", state: 0},
    ].map(item => {
      const binPath = this.getClashBinPath()
      const itemConfigPath = this.getConfigPath(item.id)
      fs.ensureFileSync(itemConfigPath)
      const content = this.getClashConfig([], {port: item.port, secret: item.secret, apiPort: item.apiPort})
      const yamlString: string = yaml.dump(content, {noRefs: true})
      fs.writeFileSync(itemConfigPath, yamlString, "utf-8")
      return {
        ...item,
        fireCmd: `${binPath} -f ${itemConfigPath}`
      }
    })
  }

  debug() {
    
  }

  getNext(groupId: string) {
    const group = this.getGroup(groupId)
  }
  
  refreshConfig() {
    return of(null).pipe(
      tap(() => ILogger.info("Start refreshConfig...")),
      concatMap(() => this.getYamlUriFromSub()),
      tap(() => ILogger.info(`Start parseYamlFromUri...`)),
      concatMap(uris => 
        forkJoin([...uris.map(uri => this.parseYamlFromUri(uri))])  
      ),
      map(res => {
        return res.reduce((acc, val) => {
          return [...acc, ...val]
        }, [])
      }),
      // /Users/eros/Documents/code/wonew/wonew-proxy-server/bin/clash -f /Users/eros/Documents/code/wonew/wonew-proxy-server/bin/config.d/config_m9DX7aXnhIBGCQPX3vyvHYfVcMRNHnUh.yaml
      // curl -x http://127.0.0.1:6001 http://www.qq.com
      map(list => {
        const validList = list.filter(e => {
          if (e.type === "wireguard") return false
          if (e.cipher !== "auto") return false
          return true
        })
        const content = this.getClashConfig(validList)
        // const filepath = `/Users/eros/.config/clash/config-${moment().format("HH_mm_ss")}.yaml`
        const filepath = this.getConfigPath(`${moment().format("YYYY_MM_DD_HH_mm_ss")}_${IHelper.hash(32)}`)
        const yamlString: string = yaml.dump(content, {noRefs: true})
        fs.writeFileSync(filepath, yamlString, "utf-8")

        // const debugpath = `/Users/eros/.config/clash/config_${key}.yaml`
        // fs.ensureFileSync(debugpath)
        // fs.writeFileSync(debugpath, yamlString, "utf-8")
        return {configPath: filepath, proxies: validList}
      }),
      tap((d) => ILogger.info(`Start reloadConfig... proxies count=${d.proxies.length}`)),
      concatMap(({configPath, proxies}) => 
        forkJoin(
          this.groups.map(e => this.reloadConfig(configPath, e))
        ).pipe(
          map(res => {
            this.proxies = [...proxies]
            this.proxyConfigPath = configPath
            return {res, configPath, proxies}
          }),
        )
      ),
      tap(() => ILogger.info(`Start checkAllDelay...`)),
      concatMap(() => this.checkAllDelay()),
      tap(() => ILogger.info(`End checkAllDelay...`)),
    )
  }

  parseYamlFromUri(uri: string): Observable<IWonewSer.ClashProxy[]> {
    return defer(() => 
      axios.get(uri, {timeout: 5 * 1000})
    ).pipe(
      map(res => {
        let config: {proxies: IWonewSer.ClashProxy[]} = yaml.load(res.data) as {proxies: IWonewSer.ClashProxy[]}
        return config.proxies.map(e => {
          return {...e, name: IHelper.hash(32)}
        }) as IWonewSer.ClashProxy[]
      }),
      catchError(() => of([])),
      tap((res) => {
        ILogger.info(`From ${uri} get ${res.length} proxies.`)
      })
    )
  }
  getYamlUriFromSub(): Observable<string[]> {
    return defer(() => 
      axios.get(`https://clashsub.com/free-subscribe`)
    ).pipe(
      map(res => {
        const html = res.data as string
        const reg = /<p>http(.+?)<\/p>/g
        const urlList = html.match(reg).map(e => e.replace("<p>", "").replace("</p>", "").trim())
        return urlList
      })
    )
  }
  getConfigPath(key: string): string {
    const targetPath = path.join(process.cwd(), `bin/config.d/config_${key}.yaml`)
    fs.ensureFileSync(targetPath)
    return targetPath
  }

  getClashBinPath() {
    return path.join(process.cwd(), `bin/clash`)
  }

  getConfigTemplate(): {[prop: string]: any} {
    const targetPath = path.join(process.cwd(), `example.yaml`)
    return yaml.load(fs.readFileSync(targetPath, "utf-8"))
  }

  getMockProxy(): IWonewSer.ClashProxy[] {
    const proxy = {
      "name": "proxy000001",
      "type": "trojan",
      "server": "iplc-hk-beta1.trojanwheel.com",
      "port": 4003,
      "password": "ysQ83fsyXBteE5eWVA",
      "alpn": [
        "h2",
        "http/1.1"
      ],
      "skip-cert-verify": true
    } as IWonewSer.ClashProxy
    return [proxy]
  }

  getClashConfig(_proxies: IWonewSer.ClashProxy[], options?: {port?: number, apiPort?: number, secret?: string}) {
    const proxies = _proxies.length <= 0 ? this.getMockProxy() : [..._proxies]
    const exampleConfig: any = this.getConfigTemplate()
    exampleConfig["mixed-port"] = options?.port || 6001
    exampleConfig["external-controller"] = `127.0.0.1:${options?.apiPort || 6002}`
    exampleConfig["secret"] = options?.secret || "azc2698*#@"
    exampleConfig.proxies = proxies
    exampleConfig["proxy-groups"][0].proxies = proxies.map(e => e.name)
    exampleConfig["proxy-groups"][1].proxies = ["Auto", ...(proxies.map(e => e.name))]
    return exampleConfig
  }

  getClashApiHeaders(secret: string): RawAxiosRequestHeaders {
    return {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${secret}`
    }
  }

  getGroup(id: string): IWonewSer.ProxyGroup {
    const group: IWonewSer.ProxyGroup = this.groups.find(e => e.id === id)
    if (!group) {
      throw new HttpException("Group not exits.", HttpStatus.BAD_REQUEST)
    }
    return group
  }

  checkAllDelay() {
    let groupId: string = this.groups[0].id
    return defer(() => 
      forkJoin(this.proxies.map(item => this.checkOneDelay(groupId, item)))
    )
  }

  checkOneDelay(groupId: string, proxy: IWonewSer.ClashProxy, timeout?: number): Observable<{status: boolean, message: string, proxy: IWonewSer.ClashProxy, data: any}> {
    const group: IWonewSer.ProxyGroup = this.getGroup(groupId)
    return defer(() => 
      axios.get(`http://127.0.0.1:${group.apiPort}/proxies/${proxy.name}/delay`, {
        params: {
          timeout: timeout || 5000,
          // url: "http://www.gstatic.com/generate_204",
          url: group.checkUrl,
        },
        headers: this.getClashApiHeaders(group.secret)
      })
    ).pipe(
      map(res => {
        if (res?.data?.delay === undefined) {
          throw new HttpException(`Get delay failed.`, HttpStatus.BAD_REQUEST)
        }
        return {status: true, data: res.data, message: "success"}
      }),
      catchError(error => {
        return of({status: false, message: error?.response?.data?.message || error.message, data: null})
      }),
      map(res => {
        proxy.delayRes = res
        return {...res, proxy}
      })
    )
  }

  switchProxy(groupId: string, proxy: IWonewSer.ClashProxy): Observable<boolean> {
    const group = this.getGroup(groupId)
    return defer(() => 
      axios.put(`http://127.0.0.1:${group.apiPort}/proxies/Proxy`, {
        name: proxy.name,
      }, {
        headers: {...this.getClashApiHeaders(group.secret)}
      })
    ).pipe(
      map((res) => {
        console.log("rsss", res.data)
        return true
      }),
    )
  }

  reloadConfig(configPath: string, group: IWonewSer.ProxyGroup) {
    return defer(() => 
      axios.put(`http://127.0.0.1:${group.apiPort}/configs`, {
        path: configPath
      }, {
        params: {
          force: false,
        },
        headers: {...this.getClashApiHeaders(group.secret)}
      })
    ).pipe(
      map(() => {
        return {status: true, message: "success"}
      }),
      catchError(error => {
        return of({status: false, message: `Reload config failed. ${error?.response?.data?.message || error.message}`})
      })
    )
  }
}

const ClashHelper = new _ClashHelper()

export default ClashHelper